package com.spring.security.app.authentication;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.security.app.oauth2.Oauth2Util;
import com.spring.security.core.properties.SecurityConstants;
import com.spring.security.core.properties.SecurityProperties;
import com.spring.security.core.support.ServerResponseEntity;
import io.netty.util.CharsetUtil;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Strategy used to handle a successful user authentication.
 * <p>
 * Implementations can do whatever they want but typical behaviour would be to control the
 * navigation to the subsequent destination (using a redirect or a forward). For example,
 * after a user has logged in by submitting a login form, the application needs to decide
 * where they should be redirected to afterwards (see
 * {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be
 * included if required.
 *
 * @author_seabed_moon
 */
@Log4j2
@Component("sunLandAuthenticationSuccessHandler")
public class SpringAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private final ObjectMapper objectMapper;

    private final SecurityProperties securityProperties;

    private final ClientDetailsService clientDetailsService;

    private final AuthorizationServerTokenServices authorizationServerTokenServices;

    private final RedisTemplate<String,Object> redisTemplate;

    private final TokenStore redisTokenStore;

    public SpringAuthenticationSuccessHandler(ObjectMapper objectMapper, SecurityProperties securityProperties, ClientDetailsService clientDetailsService, AuthorizationServerTokenServices authorizationServerTokenServices, RedisTemplate<String,Object> redisTemplate, TokenStore redisTokenStore) {
        this.objectMapper = objectMapper;
        this.securityProperties = securityProperties;
        this.clientDetailsService = clientDetailsService;
        this.authorizationServerTokenServices = authorizationServerTokenServices;
        this.redisTemplate = redisTemplate;
        this.redisTokenStore = redisTokenStore;
    }

    /**
     * Called when a user has been successfully authenticated.
     *
     * @param httpServletRequest  the request which caused the successful authentication
     * @param httpServletResponse the response
     * @param authentication      the <tt>Authentication</tt> object which was created during
     *                            the authentication process.
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
        logger.info(SecurityConstants.LOGIN_SUCCESS_MSG);
        String authorizationHeader = httpServletRequest.getHeader(SecurityConstants.DEFAULT_PARAMETER_NAME_AUTHORIZATION);
        if (null == authorizationHeader || !authorizationHeader.startsWith(SecurityConstants.DEFAULT_BASIC)) {
            throw new UnapprovedClientAuthenticationException(SecurityConstants.CLIENT_AUTHENTICATION_EXCEPTION_MSG);
        }
        ClientDetails clientDetails;
        String clientId = securityProperties.getOauth2().getClients()[0].getClientId();
        switch (authorizationHeader) {
            case SecurityConstants.DEFAULT_BASIC_APP:
                // ~ APP登录
                clientDetails = Oauth2Util.buildAppClientDetails(clientId);
                break;
            case SecurityConstants.DEFAULT_Basic_PC:
                // ~ PC登录
                clientDetails = Oauth2Util.buildPcClientDetails(clientId);
                break;
            case SecurityConstants.DEFAULT_BASIC_THREE_PARTY:
                // ~ 三方登录
                String[] tokens = extractAndDecodeHeader(authorizationHeader);
                assert tokens.length == 2;
                clientId = tokens[0];
                String clientSecret = tokens[1];
                clientDetails = clientDetailsService.loadClientByClientId(clientId);
                if (null == clientDetails) {
                    throw new UnapprovedClientAuthenticationException(StringUtils.join(SecurityConstants.ERROR_CLIENT_ID_OR_CONFIG_INFO_NOT_FOUND, clientId));
                } else if (!StringUtils.equals(clientSecret, clientDetails.getClientSecret())) {
                    throw new UnapprovedClientAuthenticationException(StringUtils.join(SecurityConstants.CLIENT_SECRET_NOT_FOUND, clientId));
                }
                break;
            default:
                throw new UnapprovedClientAuthenticationException(StringUtils.join(SecurityConstants.MISMATCHED_LOGIN_TYPE, authorizationHeader));
        }
        // ~ 单设备登录
        String singleDeviceLogin = SecurityConstants.SINGLE_DEVICE_LOGIN;
        String username = null;
        if (httpServletRequest.getServletPath().equals(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)) {
            username = httpServletRequest.getParameter(SecurityConstants.USER_NAME_KEY);
        } else if (httpServletRequest.getServletPath().equals(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE)) {
            username = httpServletRequest.getParameter(SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE);
        }
        singleDeviceLogin = StringUtils.join(singleDeviceLogin, username);
        Object oldAccessToken = redisTemplate.opsForValue().get(singleDeviceLogin);
        if (null != oldAccessToken) {
            OAuth2AccessToken oAuth2AccessToken = redisTokenStore.readAccessToken(oldAccessToken.toString());
            if (null != oAuth2AccessToken) {
                // 清除其他设备的登陆信息
                redisTokenStore.removeAccessToken(oAuth2AccessToken);
                OAuth2RefreshToken oAuth2RefreshToken = oAuth2AccessToken.getRefreshToken();
                redisTokenStore.removeRefreshToken(oAuth2RefreshToken);
                redisTokenStore.removeAccessTokenUsingRefreshToken(oAuth2RefreshToken);
            }
        }
        OAuth2AccessToken accessToken = Oauth2Util.buildOauth2AccessToken(authorizationServerTokenServices, authentication, clientId, clientDetails);
        redisTemplate.opsForValue().set(singleDeviceLogin, accessToken.getValue(), accessToken.getExpiresIn(), TimeUnit.SECONDS);
        redisTemplate.opsForValue().set(StringUtils.join(SecurityConstants.TOKEN_USER_INFO, accessToken.getValue()), JSONObject.toJSON(authentication.getPrincipal()), accessToken.getExpiresIn(), TimeUnit.SECONDS);
        String tokenBuilder = StringUtils.join(SecurityConstants.DEFAULT_PARAMETER_NAME_BEARER, accessToken.getValue());
        httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        httpServletResponse.setHeader(SecurityConstants.DEFAULT_PARAMETER_NAME_AUTHORIZATION, tokenBuilder);
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(ServerResponseEntity.success(accessToken, HttpStatus.OK.name())));
    }


    /**
     * Decodes the header into a username and password.
     *
     * @throws BadCredentialsException if the Basic header is not present or is not valid
     *                                 Base64
     */
    private String[] extractAndDecodeHeader(String header)
            throws IOException {
        byte[] base64Token = header.substring(6).getBytes(CharsetUtil.UTF_8.toString());
        byte[] decoded;
        try {
            decoded = Base64.decode(base64Token);
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException(
                    "Failed to decode basic authentication token");
        }
        String token = new String(decoded, CharsetUtil.UTF_8.toString());
        int deLim = token.indexOf(":");
        if (deLim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }
        return new String[]{token.substring(0, deLim), token.substring(deLim + 1)};
    }

}